menu
  Home  ==>  papers  ==>  oop_components  ==>  delphi_generics_tutorial   

Delphi .Net Generics Tutorial - Felix John COLIBRI.


1 - Why Generics ?

Generics (also called Parameterized types or Generic types) allow us to write more general code, while keeping type safety.

Delphi tList, tStringList, tObjectlist or tCollection can be used to build specialized containers, but require type casting. With Generics, casting is avoided and the compiler can spot type errors sooner.




2 - Life Before Generics

2.1 - Our basic example: a stack

We will illustrate the difference between usual techniques and the use of Generics with the classical bounded stack example: we want to store data on a fixed size stack using Push and Pop.



2.2 - An Integer stack

We create the stack with this simple CLASS:

unit u_c_genc_101_integer_stack;
  interface
    type c_integer_stackClass
                            m_integer_arrayArray of Integer;
                            m_top_of_stackInteger;

                            constructor create_integer_stack(p_lengthInteger);
                            procedure push(p_integerInteger);
                            function f_popInteger;
                          end// c_integer_stack

  implementation

    // -- c_integer_stack

    constructor c_integer_stack.create_integer_stack(p_lengthInteger);
      begin
        Inherited Create;
        SetLength(m_integer_arrayp_length);
      end// create_integer_stack

    procedure c_integer_stack.push(p_integerInteger);
      begin
        if m_top_of_stackLength(m_integer_array)
          then begin
              m_integer_array[m_top_of_stack]:= p_integer;
              Inc(m_top_of_stack);
            end;
      end// push

    function c_integer_stack.f_popInteger;
      begin
        if m_top_of_stack>= 0
          then begin
              Dec(m_top_of_stack);
              Result:= m_integer_array[m_top_of_stack];
            end
          else raise Exception.Create('empty') ;
      end// f_pop

  end.

and use it like this:

program p_genc_001_array_of_integer;
  uses
  SysUtils,
  u_c_genc_101_integer_stack in 'u_c_genc_101_integer_stack.pas';

  var g_c_integer_stackc_integer_stack;

  begin // main
    g_c_integer_stack:= c_integer_stack.create_integer_stack(5);

    with g_c_integer_stack do
    begin
      push(111);
      push(222);
      push(333);

      // -- refused by the compiler:
      // push('allistair');

      writeln(f_pop);
      writeln(f_pop);
      writeln(f_pop);
    end// with g_c_integer_stack
  end// main

Note that

  • since the compiler knows that we declared an ARRAY OF Integer, it immediately refuses to push any type incompatible with Integers, which is the case for Strings
  • we can build an Stack of Strings, but we then have two separate units which have to evolve and to be maintained separately


2.3 - A stack of tObject with type casting

We can generalize the previous code by using an ARRAY OF tObject:
  • the CLASS only uses the tObject type
  • the assignment rules of Delphi allows us to push any tObject or any of its descendents, like a c_person object
  • to retrieve the values, we MUST use type casting
Here is our tObject CLASS:

unit u_c_object_stack;
  interface

    type c_object_stackclass
                           m_object_arrayArray of tObject;
                           m_top_of_stackInteger;

                           constructor create_object_stack(p_lengthInteger);
                           procedure push(p_c_objecttObject);
                           function f_poptObject;
                         end// c_object_stack

  implementation
    uses SysUtils;

    // -- c_object_stack

    constructor c_object_stack.create_object_stack(p_lengthInteger);
      begin
        Inherited Create;
        SetLength(m_object_arrayp_length);
      end// create_object_stack

    procedure c_object_stack.push(p_c_objecttObject);
      begin
        if m_top_of_stackLength(m_object_array)
          then begin
              m_object_array[m_top_of_stack]:= p_c_object;
              Inc(m_top_of_stack);
            end;
      end// push

    function c_object_stack.f_poptObject;
      begin
        if m_top_of_stack>= 0
          then begin
              Dec(m_top_of_stack);
              Result:= m_object_array[m_top_of_stack];
            end
          else raise Exception.Create('empty') ;
      end// f_pop

    end// u_c_object_stack



Here are an examples of storing

  • Integers:

    procedure use_integer_stack;
      var l_c_integer_stackc_object_stack;
      begin
        l_c_integer_stack:= c_object_stack.create_object_stack(5);

        with l_c_integer_stack do
        begin
          push(tObject(111));
          push(tObject(222));

          writeln(f_pop);
          writeln(f_pop);
        end// with l_c_integer_stack
      end// use_integer_stack

  • or custom c_person objects:

    // -- c_person

    type c_personClass
                     m_first_nameString;
                     Constructor create_person(p_first_nameString);
                   end// c_person

    constructor c_person.create_person(p_first_nameString);
      begin
        Inherited Create;
        m_first_name:= p_first_name;
      end// createp_person

    // -- c_person stack

    procedure use_person_stack;
      var l_c_person_stackc_object_stack;
      begin
        l_c_person_stack:= c_object_stack.create_object_stack(5);
        with l_c_person_stack do
        begin
          push(c_person.create_person('ann'));
          // -- accepted here
          push(tObject(222));
          push(c_person.create_person('allistair'));

          writeln(c_person(f_pop).m_first_name);
          // -- poping an Integer as a c_person will fail here
          writeln(c_person(f_pop).m_first_name);
          writeln(c_person(f_pop).m_first_name);
        end// with l_c_person_stack
      end// use_person_stack




Note that
  • for the Integer stack, we had to cast the Integer value into a tObject. This transformation of a Integer litteral into a CLASS in the Win32 world is simply a notation to force the compiler to consider the Integer as a tObject. No transformation takes place. The tObject is a 4 byte pointer, and the Integer a 4 byte value. In the .Net world, things are quite different: 123 is a litteral and a VAR my_integer with value 123 is a CLASS, which has methods, like ToString. We can write my_integer.ToString, we cannot write 123.ToString. The transformation of the litteral into a CLASS is called boxing and is not free: the compiler must generate code for the conversion
  • the Writeln accepted to print the value of a tObject as an Integer value. Therefore casting is not mandatory to print the f_pop of the Integer
  • for the c_person stack
    • casting was not necessary for the assignment (c_person is "assignment compatible" with tObject
    • casting was necessary to retrieve the top of stack as a c_person
    • if we cast an Integer as a c_person, there is an exception AT RUNTIME. Thats exactly the problem: when we use casting, we force the compiler to believe that the object if of some type, and this will be blindly accepted at compile time. When we cast a wrong object, the error is detected later
  • any cell can contain any tObject descendent. So this structure is polymorphic: we can store objects of different types in the same stack, but the retrieval will necessitate a lot of testing to extract the correct type (using IS or AS)


2.4 - A .Net Collection

The previous examples were written using the Win32 personality of Delphi. We face the same problems with the .Net personality: either we write type safe code tied to a specific type, or we use casting, with the risk of encountering run time exception.

Here is an example:

program p_genc_003_array_of_object;
  uses System.Collections;

  type c_personClass
                   m_first_nameString;
                   Constructor create_person(p_first_nameString);
                 end// c_person

  // -- c_person

  constructor c_person.create_person(p_first_nameString);
    begin
      Inherited Create;
      m_first_name:= p_first_name;
    end// createp_person

  procedure use_person_arraylist;
    var l_c_person_listArrayList;
        l_c_personc_person;
    begin
      l_c_person_list:= ArrayList.Create;

      l_c_person_list.Add(c_person.create_person('Miller'));
      l_c_person_list.Add(c_person.create_person('Smith'));
      // -- this is accepted
      l_c_person_list.Add('xxx');

      // -- the 'xxx' entry will trigger an exception
      for l_c_person in l_c_person_list do
        writeln(l_c_person.m_first_name);
    end// use_person_arraylist

  begin // main
    use_person_arraylist;

    writelnwrite('=> type enter'); Readln;
  end// main

Note that:

  • we can assign the objects without type casting, since c_person is an Object descendent
  • the FOR IN construct apparently does not require casting. In fact the compiler does the casting for us, and we still get a run time exception if an object is not of the expected type


3 - The First Generics Example

3.1 - Our first Generic example

To create a generic stack
  • in the CLASS definition:
    • we add a type parameter between "<" and ">" just after the CLASS name

      type c_generic_stack<T>= class
             // ---ooo---

    • this T parameter can be placed at the same places as any TYPE: in member fields, in CONSTRUCTOR's or method's parameters, in FUNCTION's result type:

      type c_generic_stack<T>= class
                                 m_gen_arrayArray of T;
                                 m_top_of_stackInteger;

                                 constructor create_generic_stack(p_lengthInteger);
                                 procedure push(p_genT);
                                 function f_popT;
                               end// c_generic_stack


  • in the IMPLEMENTATION, we use the identifiers of type T:
    • we add the type parameter <T> after each method name. For instance:

      procedure c_generic_stack<T>.push(p_genT);
        begin
          // --ooo--

    • and we use any parameter or variables of type T in our code:

      procedure c_generic_stack<T>.push(p_genT);
        begin
          if m_top_of_stackLength(m_gen_array)
            then begin
                m_gen_array[m_top_of_stack]:= p_gen;
                Inc(m_top_of_stack);
              end;
        end// push

  • in order to use this CLASS, we must specify which concrete kind of objects we want to use in each cell of our stack
    • at the VAR declaration level, we write the concrete type between "<" and ">":

      var my_c_integer_stackc_generic_stack<Integer>;

    • this type is also repeated during the CONSTRUCTOR call:

      my_c_integer_stack:= c_generic_stack<Integer>.create_generic_stack(5);

      my_c_integer_stack.push(111);
      writeln(my_c_integer_stack.f_pop);




Here is the complete example:
  • the CLASS definition:

    unit u_c_genc_020_generic_stack;
      interface

        type c_generic_stack<T>= class
                                   m_gen_arrayArray of T;
                                   m_top_of_stackInteger;

                                   constructor create_generic_stack(p_lengthInteger);
                                   procedure push(p_genT);
                                   function f_popT;
                                 end// c_generic_stack

      implementation

        // -- c_generic_stack

        constructor c_generic_stack<T>.create_generic_stack(p_lengthInteger);
          begin
            Inherited Create;
            SetLength(m_gen_arrayp_length);
          end// create_generic_stack

        procedure c_generic_stack<T>.push(p_genT);
          begin
            if m_top_of_stackLength(m_gen_array)
              then begin
                  m_gen_array[m_top_of_stack]:= p_gen;
                  Inc(m_top_of_stack);
                end;
          end// push

        function c_generic_stack<T>.f_popT;
          begin
            if m_top_of_stack>= 0
              then begin
                  Dec(m_top_of_stack);
                  Result:= m_gen_array[m_top_of_stack];
                end
              else raise Exception.Create('empty') ;
          end// f_pop

        end// u_c_generic_stack

  • used for an Integer stack:

    procedure use_integer_stack;
      var l_c_integer_stackc_generic_stack<Integer>;
      begin
        l_c_integer_stack:= c_generic_stack<Integer>.create_generic_stack(5);
        with l_c_integer_stack do
        begin
          push(111);
          push(222);
          // -- refused by the compiler
          // push('Abigail');

          writeln(f_pop);
          writeln(f_pop);
        end// with l_c_integer_stack
      end// use_integer_stack

  • or for a c_person stack :

    // -- c_person

    type c_personClass
                     m_first_nameString;
                     Constructor create_person(p_first_nameString);
                   end// c_person

    constructor c_person.create_person(p_first_nameString);
      begin
        Inherited Create;
        m_first_name:= p_first_name;
      end// createp_person

    // -- a c_person stack

    procedure use_person_stack;
      var l_c_person_stackc_generic_stack<c_person>;
      begin
        l_c_person_stack:= c_generic_stack<c_person>.create_generic_stack(5);
        with l_c_person_stack do
        begin
          push(c_person.create_person('ann'));
          push(c_person.create_person('allistair'));
          // -- refused by the compiler
          // Push(111);

          writeln(f_pop.m_first_name);
          writeln(f_pop.m_first_name);
        end// with l_c_person_stack
      end// use_person_stack




3.2 - Terminology

Just a couple of naming conventions:
  • the identifier located after the TYPE name (T in our case) is called the "type parameter" (or sometimes the "formal type parameter")
  • the real concrete type that we give when we want to declare a VAR or a parameter (Integer in our case), is called the "type argument" (or sometimes the "actual parameter")
  • c_stack<t_gen> is an open constructed type, and c_stack<Integer> a closed constructed type


Please note that
  • the identifier of the type parameter is indifferent: any Pascal identifier is acceptable. For very general CLASSes, it is often a single letter, like T, but using longer names, like measure or content is also correct.

    type c_currency<NV>= Class
                           end;

         c_dictionary<keyvalue>= Class
                                   end;

         c_calculator<T_valueT_operation>= Class
                                             end;

    For long names, we will use a convention where the type parameter starts with an upper case T_ followed by some meaningfull name, like T_measure

  • the compiler rejects the definition of a second CLASS with the same name but with parameter types having a different name only. It is possible, as will be shown later, to uses several parameter types. If the "parameter signature" is different, then we can use the same CLASS name

    type c_stack<T_gen>= Class
                         end;

         // NO : c_stack<T_gen_2>= Class

         c_stack<T_gen_1T_gen_2>= Class
                                    end;                                  




3.3 - How are Generics implemented ?

Generics are implemented at the .Net intermediate language level (IL: Intermediate Language= C# pseudo code) and the CLR level (Common Language Runtime: the library managing the code, containing, the IL-to-native compiler, the type checker, the loader, the memory manager etc).

The intermediate language contains :

  • the parameterized types, along with the standard types
  • markers for the type arguments
  • informations about generics included in the IL meta data
When the intermeditate code is compiled into binary code (x386 assembler)
  • when the code defines type arguments, the metadata is used to update the generic metatdata with the argument metadata
  • the compiler can then perform its type checking
  • if the type argument is a value type (Integer, Double etc), the parameters are replaced with the actual type, and the corresponding code is generated. Therefore, there is no boxing / unboxing for those actual types. In addition, if the type is used in some other places, the compiler uses a reference pointing to the compiled code
  • if the type argument is a reference type (classe, arrays, lists etc), the type parameter is replaced by tObject. The native code uses a reference pointing to the object, and this without any casting.



4 - Using Parameterized Types

4.1 - Generic CLASSes

As presented before, a CLASS can use a generic parameter



4.2 - Generic RECORDs

It is also possible to parameterize RECORDs. Here is an example:

program p_genc_032_record;
  uses SysUtils;

  type t_point<T_coordinate>= Record
                                m_xm_ym_zT_coordinate;
                              end// t_point

  var g_centert_point<Integer>;
      g_projectiont_point<Double>;

  begin // main
    g_center.m_x:= 100;
    g_projection.m_x:= 3.1415;

    writeln(g_center.m_x);
    writeln(g_projection.m_x);
  end// main


4.3 - Generic ARRAYs

The cell type of an ARRAY can use parameters:

program p_genc_032_array;
  uses SysUtils;

  type t_array<T_cell>= Array of T_cell;
       t_xy_array<T_coordinate>= Array of Array of T_coordinate;

       t_average_array<T_value>= Array[1..5] of T_value;

  var g_countst_array<Integer>;
      g_measurest_array<Double>;

      g_indexinteger;

      g_xy_arrayt_xy_array<Double>;

      g_array_of_doubleArray of Double;

  begin // main
    SetLength(g_counts, 100);
    for g_index:= 0 to 99 do
      g_counts[g_index]:= Random(100);

    SetLength(g_measures, 20);
    for g_index:= 0 to 19 do
      g_measures[g_index]:= 3.14* Random;

    SetLength(g_xy_array, 10, 20);
    g_xy_array[2, 3]:= 3.14;

    SetLength(g_array_of_double, 10* 20);
    g_array_of_double[2* 10+ 3]:= g_xy_array[2, 3];
  end// main



In the previous example, we showed

  • two dimensional ARRAYs
  • an example of mixing parameterized ARRAYs and usual ARRAYs


4.4 - Generic Procedural Types

We can use parameter types in the definition of Procedural Types.

The definition of some binary operator could be:

unit u_genc_034_procedural_type;
  interface

    Type t_pg_handle_two<T>= Procedure(p_onep_twoT);

  implementation

    end// u_genc_034_procedural_type

and we could apply this operator to Integers or Doubles:

procedure convert_two_integer(p_valuep_rateInteger);
  begin
    writeln(p_value' div 'p_rate' ='p_value Div p_rate);
  end// convert_two_integer

procedure convert_two_double(p_valuep_rateDouble);
  begin
    writeln(p_value' / 'p_rate' ='p_value / p_rate);
  end// convert_two_double

procedure use_binary_operator;
  var l_pg_convert_two_integert_pg_handle_two<Integer>;
      l_pg_convert_two_doublet_pg_handle_two<Double>;
  begin
    l_pg_convert_two_integer:= convert_two_integer;
    l_pg_convert_two_integer(20, 3);

    l_pg_convert_two_double:= convert_two_double;
    l_pg_convert_two_double(20.0, 3.0);
  end// use_binary_operator

And you will notice that the Integer division uses DIV whereas the Double division requires /.



Procedural types are usually employed to apply some handling to a set of values, and give some kind of "Lisp applicative" flavor to our programs. We define generic handling routines, and can specify AT RUNTIME, which concrete procedure will be applied.

Here is an example of a list of values with a generic handling routine:

unit u_genc_035_apply_procedural_type;
  interface

    Type t_fg_handle_one<T>= Function(p_valueT): T;
         c_vector<T>= Class
                        m_vectorArray[0..9] of T;

                        constructor Create;
                        procedure compute(p_fg_handle_onet_fg_handle_one<T> );
                        procedure display_vector;
                      end;

  implementation
    uses SysUtils;

    // -- c_vector<T>

    constructor c_vector<T>.Create;
      begin
        Inherited;
      end// Create

    procedure c_vector<T>.compute(p_fg_handle_onet_fg_handle_one<T>);
      var l_indexInteger;
      begin
        for l_index:= 0 to 9 do
          m_vector[l_index]:= p_fg_handle_one(m_vector[l_index]);
      end// compute

    procedure c_vector<T>.display_vector;
      var l_indexInteger;
          l_resultString;
      begin
        l_result:= '';
        for l_index:= 0 to 9 do
          l_result:= l_resultm_vector[l_index].ToString' ';
        writeln(l_result);
      end// display_vector

    end// u_genc_034_procedural_type

and we can use it like this:

function f_integer_square(p_valueInteger): Integer;
  begin
    Result:= p_valuep_value;
  end// f_integer_square

procedure use_vector;
  var l_c_vectorc_vector<Integer>;
      l_indexInteger;
  begin
    l_c_vector:= c_vector<Integer>.Create;
    for l_index:= 0 to 9 do
      l_c_vector.m_vector[l_index]:= l_index;
    l_c_vector.display_vector;

    l_c_vector.compute(f_integer_square);
    l_c_vector.display_vector;
  end// l_c_vector

Note that:

  • this is the first case where we start "combining" the generic elements
  • to display the values, we could call Writeln(my_T_instance.ToString) since the compiler assumes that any type T has a ToString function


4.5 - Generic Events (PROCEDURE OF OBJECT)

Events are very similar to procedural type, but can only be defined in CLASSes. The inner working is the same, but the compiler simply pushes an additional transparent parameter which is the object which called the procedure. In addition, Events are more used to acknowledge the user of a CLASS that something is happening rather than apply some functional computation. They are in general used as some kind of callback (the mouse has been clicked, a character arrived from the network etc).

We define here a The definition of a PROCEDURE OF OBJECT which will notify us about any changes:

unit u_genc_035_procedure_of_object;
  interface

    type t_po_notify_change<T>= Procedure(p_valueTOf Object;

         c_storage<V>= Class
                         m_tablearray of V;

                         m_on_notify_value_changedt_po_notify_change<V>;
                         m_on_notify_storage_changedt_po_notify_changec_storage<V> >;

                         constructor create_storage(p_sizeInteger);
                         procedure add_value(p_indexIntegerp_valueV);
                       end// c_storage

  implementation

    // -- Type c_storage<V>

    constructor c_storage<V>.create_storage(p_sizeInteger);
      begin
        Inherited Create;
        SetLength(m_tablep_size);
      end// create_storage

    procedure c_storage<V>.add_value(p_indexIntegerp_valueV);
      begin
        m_table[p_index]:= p_value;

        if Assigned(m_on_notify_value_changed)
          then m_on_notify_value_changed(p_value);

        if Assigned(m_on_notify_storage_changed)
          then m_on_notify_storage_changed(Self);
      end// add_value

    end// u_genc_035_procedure_of_object

and we can use the event like this:

program p_genc_035_procedure_of_object;
  uses SysUtils,
    u_genc_035_procedure_of_object in 'u_genc_035_procedure_of_object.pas';

  Type c_statisticsClass
                       m_c_storagec_storage<Integer>;

                       constructor create_statistics;
                       procedure display_value_changed(p_valueInteger);
                       procedure display_storage_changed(p_c_storagec_storage<Integer>);
                     end// c_storage

  // -- Type c_statistics

  constructor c_statistics.create_statistics;
    begin
      Inherited Create;

      m_c_storage:= c_storage<Integer>.create_storage(5);

      m_c_storage.m_on_notify_value_changed:= display_value_changed;
      m_c_storage.m_on_notify_storage_changed:= display_storage_changed;
    end// create_statistics

  procedure c_statistics.display_value_changed(p_valueInteger);
    begin
      Writeln('added_value 'p_value);
    end// display_value_changes

  procedure c_statistics.display_storage_changed(p_c_storagec_storage<Integer>);
    var l_indexInteger;
    begin
      Writeln('added_value_to ');
      for l_index:= 0 to Length(p_c_storage.m_table)- 1 do
        Writeln('  'p_c_storage.m_table[l_index]);
    end// display_storage_changed

  var g_c_statisticsc_statistics;

  begin // main
    g_c_statistics:= c_statistics.create_statistics;
    g_c_statistics.m_c_storage.add_value(0, 33);
  end// main

Note that

  • since events must be nested inside CLASSes, we had to create one for our example (c_statistics). In standard events handling, it is the tForm which harbors the delegated events like tButtonClick


4.6 - Generic Methods

We can generalize the methods of a CLASS:

unit u_c_genc_36_generic_method;
  interface

    type c_productclass
                      m_priceDouble;
                      m_quantityInteger;

                      Constructor create_product(p_priceDoublep_quantityInteger);
                      procedure display<T>(p_textStringp_valueT);
                    end// c_product

  implementation

    // -- c_product

    Constructor c_product.create_product(p_priceDoublep_quantityInteger);
      begin
        Inherited Create;

        m_price:= p_pricem_quantity:= p_quantity;
      end// create_product

    procedure c_product.display<T>(p_textStringp_valueT);
      begin
        writeln(p_textp_value.ToString);
      end// display

    end// u_c_genc_36_generic_method

and we use it here:

procedure use_generic_method;
  var l_c_productc_product;
  begin
    l_c_product:= c_product.create_product(218.15, 34);

    l_c_product.display<Double>( 'the price is    'l_c_product.m_price);
    l_c_product.display<Integer>('the quantity is 'l_c_product.m_quantity);
  end// use_generic_method



We can also have parameterized CLASS methods. Those methods can be called by directly calling the CLASS method, without having to create an object.

First, here is the CLASS

unit u_c_genc_36_generic_class_method;
  interface

    type c_sortClass
                   Class procedure swap<T>(Var pv_onepv_twoT); Static;
                 end// c_sort

  implementation

    // -- c_sort

    Class procedure c_sort.swap<T>(Var pv_onepv_twoT);
      var l_temporaryT;
      begin
        l_temporary:= pv_two;
        pv_two:= pv_one;
        pv_one:= l_temporary
      end// swap

    end// u_c_genc_36_generic_class_method

and an example of how to use this swap CLASS method:

procedure use_generic_class_method;
  var l_entier_1l_entier_2Integer;
      l_double_1l_double_2Double;
  begin
    l_entier_1:= 10; l_entier_2:= 200;
    writeln('original 'l_entier_1: 5, l_entier_2: 5);
    c_sort.swap<Integer>(l_entier_2l_entier_1);
    writeln('    swap 'l_entier_1: 5, l_entier_2: 5);
    writeln;
    l_double_1:= 1.11; l_double_2:= 333.33;
    writeln('original 'l_double_1: 7: 2, ' 'l_double_2: 7: 2);
    c_sort.swap<Double>(l_double_1l_double_2);
    writeln('    swap 'l_double_1: 7: 2, ' 'l_double_2: 7: 2);
  end// use_generic_class_method

Note that

  • the classical CLASS method in Delphi is the CONSTRUCTOR which is called using the c_class.Create typical syntax
  • in the .Net world, the CLASS methods are much more important. C# is a Java clone, and in Java, everything is a CLASS. There are no global PROCEDUREs or FUNCTIONs: they necessarily belong to some CLASS. So if you want to compute a Sine, you have to nest this in some CLASS. This has then been pushed to the extreme, where the CLASS is used as a basic encapsulation mechanism. In .Net, the System.IO.File is a CLASS containing ONLY CLASS methods, like File.Copy. In Dbx4, many elements are definined in a CLASS. For instance the TDBXDataTypes.DateType is a CONST defined in a CLASS, and CLASS methods are used in many places, for factories (TDBXConnectionFactory.OpenConnectionFactory) or to get values which do not use the CLASS attributes (TDBXValueTypeEx.DataTypeName).


4.7 - Generic INTERFACEs

In addition to CLASSes, INTERFACEs can also use generics. A simple visitor could be defined with:

unit u_i_genc_037_generic_interface;
  interface

    type i_visitor<T_node>= Interface
                              procedure Visit(p_nodeT_node);
                            end// i_visitor

         c_point<T_data>= Class
                            m_xm_yT_data;

                            constructor create_point(p_xp_yT_data);
                            function ToStringStringOverride;
                            procedure Accept(p_c_visitori_visitorc_point<T_data> > );
                          end// c_point

         c_display_visitor<T_node>= Class(tObjecti_visitor<T_node>)
                                      procedure Visit(p_nodeT_node);
                                    end// c_display_visitor

  implementation

    // -- c_point<T_data>

    constructor c_point<T_data>.create_point(p_xp_yT_data);
      begin
        Inherited Create;
        m_x:= p_xm_y:= p_y;
      end// create_point

    procedure c_point<T_data>.Accept(p_c_visitori_visitorc_point<T_data> > );
      begin
        p_c_visitor.Visit(Self);
      end// Accept

    function c_point<T_data>.ToStringString;
      begin
        writeln('x='m_x.ToString', y=' + m_y.ToString);
      end// ToString

     // -- c_display_visitor<T_node>

     procedure c_display_visitor<T_node>.Visit(p_nodeT_node);
       begin
         writeln(p_node.ToString);
       end// Visit

    end// u_i_genc_037_generic_interface_5

and here is an example using this visitor :

program p_genc_037_generic_interface;
  uses SysUtils,
      u_i_genc_037_generic_interface in 'u_i_genc_037_generic_interface.pas';

  var g_c_pointc_point<Integer>;
      g_c_display_visitorc_display_visitorc_point<Integer> >;

  begin // main
    g_c_point:= c_point<Integer>.create_point(3, 50);

    g_c_display_visitor:= c_display_visitorc_point<Integer> >.Create;

    g_c_point.Accept(g_c_display_visitor);
  end// main  end. // main



Note that

  • this is a very downgraded "visitor". In the classical Visitor design pattern, we usually have
    • a structure (a document built from different elements, all descending from the same basic abstract CLASS)
    • several visitors which "visit" each element of the structure, performing different specific tasks
    The UM Class Diagram of such a bona fide visitor would be:

    genc_visitor

    Nevertheless, the possibility to use generic types for the visitor INTERFACE is a very nice improvement.

  • generic types are very often associated with Design Patterns. This is not a surprise, since one of the basic mechanism of Design Patterns is to define an INTERFACE, and let the user select at RUNTIME any descendent which implements this INTERFACE.

  • generic INTERFACEs are also often used with CONSTRAINTs, which are our next topic.



5 - Constraints on Generic Types

5.1 - Operations on Generic Types

At first glance, generics look like a technique of choice for building numerical libraries: computing averages, min values, max values, series handling, matrix computations.

This is not directly possible, and the reason is easy to understand: when we declare an ARRAY OF T, the compiler has no information whatsoever about T. Therefore it cannot generate any code for adding, comparing, multiplying T values. A TYPE specifies the possible values and the allowed operators:

  • for Integers, values are numbers without fractional part: 3, -15, 0, and operators +, -, * ,DIV
  • for Reals, values are numeric values with fractional part: 3.14, -12.23e-18, and operators +, -, *, /
Now when the compilers sees T, there is no indication as to what the future argument will be. It could be Integer (with +, *, DIV), Double (with +, -, but / and not DIV), String (with only +) or any CLASS (c_person, with no arithmetic operation whatsoever).

The bottom line is that in order to perform some meaningfull handling on the data of type T, we must tell the compiler which operations will be available for the T concrete arguments. We somehow reduce the very general type T to some type subset, where, for instance, addition, or comparison are possible. We impose constraints on the type parameter T.

What kind of constraints ? Currently, we can constraint T

  • to implement some INTERFACE
  • to be a CLASS (as opposed to be value type)
  • to be a descendent of some specific CLASS
  • to have a default parameterless CONSTRUCTOR


5.2 - INTERFACE constraint

We will first start with the simple = operator. We want to be able to locate a value in a structure. So we must be able to tell whether two values are the same. In the .Net world, this is defined by the iEquatable INTERFACE, defined in the .Net Help (.Net Framework SDK | Class Library | System | iEquatable) :

interface

Any CLASS implementing iComparable will offer a Equates() FUNCTION that we can use like this:

IF p_c_one.Equates(p_c_two)
  THEN ...ooo

The current Delphi syntax for imposing a constraint is to specify it after the type parameter identifier:

Type c_my_class <T : my_constraint > = Class
                                         // -- ...ooo... use T
                                       end// c_my_class

and for INTERFACE constraints, we simply indicate the name of which INTERFACE the concrete type arguments will implement, like this:

Type c_my_class < T : i_my_interface < T > > = Class
                                               // -- ...ooo... use T
                                             end// c_my_class



Here is a container CLASS with an ARRAY of generic values, and a f_index_of FUNCTION:

unit u_c_genc_041_find_array;
  interface

    type c_find_array<T_dataiEquatable<T_data> >=
             class
               m_arrayArray of T_data;
               m_countInteger;

               constructor create_find_array(p_lengthInteger);
               procedure add_to_array(p_cellT_data);
               function f_index_of(p_cellT_data): Integer;
             end// c_find_array

  implementation

    // -- c_find_array

    constructor c_find_array<T_data>.create_find_array(p_lengthInteger);
      begin
        Inherited Create;
        SetLength(m_arrayp_length);
      end// create_find_array

    procedure c_find_array<T_data>.add_to_array(p_cellT_data);
      begin
        if m_countLength(m_array)
          then begin
              writeln('  at 'm_count' add 'p_cell.ToString);
              m_array[m_count]:= p_cell;
              Inc(m_count);
            end
          else Raise Exception.Create('no_more_room');
      end// add_to_array

    function c_find_array<T_data>.f_index_of(p_cellT_data): Integer;
      var l_indexInteger;
      begin
        Result:= -1;

        for l_index:= 0 to m_count- 1 do
        begin
          if m_array[l_index].Equals(p_cell)
            then begin
                Result:= l_index;
                Break;
              end;
        end// for l_index
      end// f_pop

  end.



When we want to use this generic CLASS, we have to specify as a type argument a TYPE which implements iEquatable. It turns out that in .Net, all the usual value types are equatable: Integer, Double, String etc.

So here is an example with Integers:

program p_genc_041_interface_equatable;
  uses SysUtils,
      u_c_genc_041_find_array in 'u_c_genc_041_find_array.pas';

  var g_c_integer_arrayc_find_array<Integer>;

  begin // main
    g_c_integer_array:= c_find_array<Integer>.create_find_array(10);

    with g_c_integer_array do
    begin
      add_to_array(111);
      add_to_array(222);
      add_to_array(333);

      writeln;
      writeln('index of 333 : 'f_index_of(333));
      writeln('index of 777 : 'f_index_of(777));
    end// with g_c_integer_array
  end// main



Please note that:

  • we had to repeat the type parameter in the CLASS header:

    type c_find_array<T_dataiEquatable<T_data> >= class

    The reason is that iEquatable is a "Generic Interface", as testified by the help snapshot above. For some INTERFACEs, like iComparable, there are two flavors: a generic one and a non generic one

  • the Help snapshot also displays other INTERFACE which could be used in constraints, like iClonable, or iComparable which we will use in the next example.


5.3 - The iComparable INTERFACE constraint

To offer a generic sort CLASS, we will use separate cell and structure CLASSes. The cell contains the payload, and the structure offers the container possibility and the sort of the cells. To be able to compare two T values, we will force T to implement iComparable.

The generic CLASSes look like this:

unit u_c_genc_042_sort_linked_list;
  interface

    type c_cell<T_data>=
             class
               m_dataT_data;
               m_c_next_cellc_cell<T_data>;

               constructor create_generic_cell(p_keyT_data);
             end// c_cell

         c_linked_list<T_dataiComparable<T_data> >=
             class
               m_c_first_cellc_cell<T_data>;

               constructor create_linked_list;
               procedure add_key(p_keyT_data);
               procedure list_generic_list;

               procedure do_bubble_sort;
             end// c_linked_list

  implementation
    uses SysUtils;

    // -- c_cell<T_data

    constructor c_cell<T_data>.create_generic_cell(p_keyT_data);
      begin
        Inherited Create;
        m_data:= p_key;
      end// create_generic_cell

    // -- c_linked_list<T_data: iComparable<tg_comparable_key> >

    constructor c_linked_list<T_data>.create_linked_list;
      begin
        Inherited Create;
      end// create_linked_list

    procedure c_linked_list<T_data>.add_key(p_keyT_data);
      var l_c_generic_cellc_cell<T_data>;
      begin
        l_c_generic_cell:=
            c_cell<T_data>.create_generic_cell(p_key);

        // -- link into the chain
        l_c_generic_cell.m_c_next_cell:= m_c_first_cell;
        m_c_first_cell:= l_c_generic_cell;
      end// add_key

    procedure c_linked_list<T_data>.list_generic_list;
      var l_c_current_cellc_cell<T_data>;
      begin
        l_c_current_cell:= m_c_first_cell;
        while l_c_current_cell<> Nil do
        begin
          with l_c_current_cell do
            Writeln(m_data.ToString);
          l_c_current_cell:= l_c_current_cell.m_c_next_cell;
        end;
      end// list_generic_list

    procedure c_linked_list<T_data>.do_bubble_sort;
      var l_did_swap_some_cellBoolean;
          l_c_previous_cellc_cell<T_data>;
          l_c_current_cellc_cell<T_data>;
          l_c_exchange_cellc_cell<T_data>;

          l_iterationInteger;
      begin
        if (m_c_first_cellNilor (m_c_first_cell.m_c_next_cellNil)
          then Exit;

        // -- repeat until no more swapping
        l_iteration:= 1;
        repeat
          writeln;
          writeln('iteration 'l_iteration.ToString);
          Inc(l_iteration);

          l_c_previous_cell:= Nil;
          l_c_current_cell:= m_c_first_cell;
          l_did_swap_some_cell:= false;

          while l_c_current_cell.m_c_next_cell<> Nil do
          begin
            Writeln('  cur 'l_c_current_cell.m_data.ToString
              ' next 'l_c_current_cell.m_c_next_cell.m_data.ToString);
            if l_c_current_cell.m_data.CompareTo(
                l_c_current_cell.m_c_next_cell.m_data) > 0
              then begin
                  Writeln('    swap');
                  l_c_exchange_cell:= l_c_current_cell.m_c_next_cell;
                  l_c_current_cell.m_c_next_cell:=
                      l_c_current_cell.m_c_next_cell.m_c_next_cell;
                  l_c_exchange_cell.m_c_next_cell:= l_c_current_cell;

                  if l_c_previous_cellNil
                    then m_c_first_cell:= l_c_exchange_cell
                    else l_c_previous_cell.m_c_next_cell:= l_c_exchange_cell;

                  l_c_previous_cell:= l_c_exchange_cell;
                  l_did_swap_some_cell:= true;
                end
              else begin
                  l_c_previous_cell:= l_c_current_cell;
                  l_c_current_cell:= l_c_current_cell.m_c_next_cell;
                end;
          end// while
        until not l_did_swap_some_cell;
      end// do_bubble_sort

    end// u_c_generic_stack

the interesting part, of course, it the comparison of two generic values:

If l_c_current_cell.m_data.CompareTo(l_c_current_cell.m_c_next_cell.m_data) > 0
  Then



To use this generic structure, we will use a c_person CLASS with a m_first_name String. To qualify as an element which can be used in our generic structure, it has to implement the iComparable INTERFACE, and we chose to compare two persons based on their first name:

type c_personClass(tObjectiComparable<c_person>)
                 m_first_nameString;

                 function CompareTo(p_c_personc_person): Integer;
               end// c_person

function c_person.CompareTo(p_c_personc_person): Integer;
  begin
    if m_first_namep_c_person.m_first_name
      then Result:= -1
      else Result:= + 1;;
  end// CompareTo



Here is the full code:

program p_genc_042_i_comparable;
  uses SysUtils,
  u_c_genc_042_sort_linked_list in 'u_c_genc_042_sort_linked_list.pas';

  // -- c_person
  type c_personClass;
       c_personClass(tObjectiComparable<c_person>)
                   m_first_nameString;
                   Constructor create_person(p_first_nameString);
                   function CompareTo(p_c_personc_person): Integer;
                   function ToStringStringOverride;
                 end// c_person

  constructor c_person.create_person(p_first_nameString);
    begin
      Inherited Create;
      m_first_name:= p_first_name;
    end// createp_person

  function c_person.CompareTo(p_c_personc_person): Integer;
    begin
      if m_first_namep_c_person.m_first_name
        then Result:= -1
        else Result:= + 1;;
    end// CompareTo

  function c_person.ToStringString;
    begin
      Result:= m_first_name;
    end// ToString

  // -- using persons

  var g_c_person_sorted_listc_linked_list<c_person>;

  begin // main
    g_c_person_sorted_list:= c_linked_list<c_person>.create_linked_list;

    with g_c_person_sorted_list do
    begin
      add_key(c_person.create_person('aaa'));
      add_key(c_person.create_person('zzz'));
      add_key(c_person.create_person('mmm'));

      list_generic_list;

      writeln;
      do_bubble_sort;

      writeln;
      list_generic_list;
    end// with g_c_person_sorted_list
  end// main



Now the comments:

  • the use of a linked pointed list (each cell containing a reference to the next cell) is not the most natural choice in Object Oriented Programming, with all those Collections, ArrayLists, or dynamic arrays around.
  • we also must apologize for using a Bubble Sort. But for linked list, this is easier (less lines of code) to handle than using a pivot and two search values with quick sort.
  • we only imposed the iComparable constraint on the type parameter of the structure, not of the basic cell.
  • the generic c_cell CLASS is only a placeholder. The real CLASS is the c_person CLASS. The resulting implementation is somehow complicated, as we already mentioned:

    sort_list

  • in order to be able to impose the iComparable<c_person> constraint, we had to first create a forward c_person CLASS:

    type c_personClass;
         c_personClass(tObjectiComparable<c_person>)
                     m_first_nameString;
                     // -- ...ooo...

  • and since the constraint is on the GENERIC iComparable INTERFACE, we had to specify the <c_person> type argument when we defined the INTERFACE in the inheritance portion of the c_person CLASS:

    type c_personClass(tObjectiComparable<c_person>)

  • we had also to implement the c_person.ToString FUNCTION


5.4 - INTERFACE constraint on a method type parameter

We can also impose an INTERFACE constraint on the type parameters of methods. Here is a CLASS with two parameterized methods:

unit u_c_genc_043_min_max_i_comparable;
  interface
    uses System.Collections.Generic;

    type c_min_max=
             class
               class function f_min<TIComparable<T> >(p_onep_twoT): Tstatic;
               class function f_max<TIComparable<T> >(p_onep_twoT): Tstatic;
             end// c_min_max

  implementation

    // -- c_min_max

    class function c_min_max.f_min<T>(p_onep_twoT): T;
      begin
        if p_one.CompareTo(p_two)< 0
          then Result := p_one
          else Result := p_two;
      end// f_min

    class function c_min_max.f_max<T>(p_onep_twoT): T;
      begin
        if p_one.CompareTo(p_two)> 0
          then Result := p_one
          else Result := p_two;
      end// f_max

    end// u_c_min_max

which can be used like this:

program p_genc_043_i_comparable;
  uses SysUtils,
  u_c_genc_043_min_max_i_comparable in 'u_c_genc_043_min_max_i_comparable.pas';

  var g_oneg_twoInteger;
      g_employeeg_salesmanString;

  begin // main
    g_one:= 10; g_two:= 222;
    writeln('Min of 'g_one' and 'g_two' is : ',
      c_min_max.f_min<Integer>(g_oneg_two));

    g_employee:= 'Smith'g_salesman:= 'Allistair';
    writeln('Max of 'g_employee' and 'g_salesman' is : ',
      c_min_max.f_max<String>(g_employeeg_salesman));
  end// main



Note that

  • since we used CLASS FUNCTIONs, we could directly call those methods without having to create an object of type c_min_max before


5.5 - CONSTRUCTOR constraint

Sometime we want to be able to create an object of type T. This is helpful, for instance, when we want to create an element and add it to some structure.

We can force the type parameter to have a CONSTRUCTOR by using a constraint with the CONSTRUCTOR name:

type c_my_class<T : Constructor > = class

CONSTRUCTOR constraints can be associated with INTERFACE constraints. In fact, we could not find an interesting example of a "pure constructor" constraint.



To demonstrate this constraint, we will present some kind of Factory Design Pattern. Imagine a product made of different parts, with different models containing the same parts but not in the same order:

schema

To build the different makes, we can

  • let the user assemble the parts whenever the model is created
  • or use helper classes which contain the rules pertaining to each model
From an UML standpoint, this looks like this:

genc_factory



Here is our factory CLASS :

unit u_c_genc_406_factory;
  interface

    type c_base<T_data>=
           class
             m_dataT_data;

             procedure initialize_base(p_dataT_data); Virtual;
             function ToStringStringOverride;
           end// c_base

         c_factory<T_dataB_basec_base <T_data>, Constructor >=
           class
             class function f_c_create_base(p_dataT_data): B_base;
           end// c_factory

  implementation

    // -- c_base<T_data>

    procedure c_base<T_data>.initialize_base(p_dataT_data);
      begin
        writeln('initialize 'Self.ClassName' with 'p_data.ToString);
        m_data:= p_data;
      end// initialize_base

    function c_base<T_data>.ToStringString;
      begin
        Result:= m_data.ToString;
      end// ToString

    // -- c_factory

    Class Function c_factory<T_dataB_base>.f_c_create_base(p_dataT_data): B_base;
      begin
        Result:= B_base.Create;
        Result.initialize_base(p_data);
      end// create_factory

    end// u_c_factory

The important part is the last function, which create the object and calls one of its methods.



Here we use the factory to build an Integer CLASS:

type c_integer_base=
         Classc_base<System.Int32> )
           public
             m_value_1String;

             constructor Create;
             procedure initialize_base(p_dataSystem.Int32); Override;
             function ToStringStringOverride;
         end// c_integer_base

constructor c_integer_base.Create;
  begin
    Inherited;
  end// Create
    
procedure c_integer_base.initialize_base(p_dataSystem.Int32);
  begin
    Inherited;
    m_value_1:= '111';
  end// initialize_base

function c_integer_base.ToStringString;
  begin
    Result:= Inherited ToString' 'm_value_1.ToString;
  end// ToString

// -- use the factory

procedure create_integer_base;
  var l_c_integer_basec_base<System.Int32>;
  begin
    l_c_integer_base:= c_factory<System.Int32c_integer_base>.f_c_create_base(123);
    writeln(l_c_integer_base.ToString);
  end// create_integer_base

or to build a c_person CLASS:

type c_person=
         Class
           m_first_nameString;
           Constructor Create;
           constructor create_person(p_first_nameString);
           function ToStringStringOverride;
         end// c_person

constructor c_person.create;
  begin
    Inherited Create;
  end// create

constructor c_person.create_person(p_first_nameString);
  begin
    Inherited Create;
    m_first_name:= p_first_name;
  end// create_person

function c_person.ToStringString;
  begin
    Result:= m_first_name;
  end// ToString

type c_person_base=
         Classc_base<c_person> )
           public
             m_value_1Double;

             Constructor Create;
             procedure initialize_base(p_datac_person); Override;
             function ToStringStringOverride;
         end// c_person_base

Constructor c_person_base.Create;
  begin
    Inherited;
  end// Create

procedure c_person_base.initialize_base(p_datac_person);
  begin
    Inherited;
    m_value_1:= 3.14;
  end// initialize_base

function c_person_base.ToStringString;
  begin
    Result:= Inherited ToString' 'm_value_1.ToString;
  end// ToString

procedure create_person_base;
  var l_c_person_basec_base<c_person>;
  begin
    l_c_person_base:= c_factory<c_personc_person_base>.f_c_create_basec_person.create_person('Joe') );
    writeln(l_c_person_base.ToString);
  end// create_person_base



The calls to create the objects look a little bit contrieved:

my_c_integer_base:= 
    c_factory<Int32c_integer_base>.f_c_create_base(123);

my_c_person_base:= 
    c_factory<c_personc_person_base>.f_c_create_basec_person.create_person('Joe') );

but the user who creates the object is not involved in the detail of the creation, and thats what factories are all about !



5.6 - iEnumerable Linked List

Lets conclude the presentation with an enumerable linked list.

In the .Net world, the containers are not based on the Win32 tList construct, but on the iEnumerable INTERFACE. This INTERFACE mainly offers the GetEnumerator FUNCTION, which returns an Enumerator with the MoveNext and Current members. A partial UML Class Diagram would be:

genc_ienumerable

As an example, the Ado.Net DataTable contains Rows which are collections of DataRows, and we can write code like:

var my_i_row_enumeratoriEnumerator;

  my_i_row_enumerator:= my_c_datatable.Rows.GetEnumerator;
  while m_i_row_enumerator.MoveNext do
    with m_i_row_enumearator.Current as DataRow do
      // -- perform some handling on the row

  



Following the previous snippet, our main program will be similar to:

program p_407_genc_ienumerable;
  uses SysUtils,
      System.Collections,
      System.Collections.Generic,
      u_c_genc_407_linked_ienumerable_list in 'u_c_genc_407_linked_ienumerable_list.pas';

  procedure cg_enumerator__demo;
    var l_c_cellcg_cell<String>;
        l_cg_cell_enumeratorcig_cell_enumerator<String>;
    begin
      l_c_cell:= cg_cell<String>.create_cell('aaa'Nil);
      l_c_cell:= cg_cell<String>.create_cell('bbb'l_c_cell);
      l_c_cell:= cg_cell<String>.create_cell('ccc'l_c_cell);

      l_cg_cell_enumerator:= cig_cell_enumerator<String>.create_cell_enumerator(l_c_cell);
      with l_cg_cell_enumerator do
        while MoveNext do
          writeln(Current.m_first_name.ToString);
    end// cg_enumerator__demo

  begin // main
    cg_enumerator__demo;
  end// main



To implement our list, we created the following CLASSes:

  • the basic c_cell is without surprise:

    type cg_cell<T> =
             class(System.Object)
               m_first_nameT;
               m_c_next_cellcg_cell<T>;

               constructor create_cell(p_first_nameTp_c_next_cellcg_cell<T>);
             end// c_find_list

  • the linked list will use this cell, and add the enumeration capability:

    type cg_list<Tcg_cell<T> >=
             class(System.ObjectiEnumerable<cg_cell<T> > )
               private
                 m_c_first_cellcg_cell<T>;
               public
                 constructor create_list;

                 function GetEnumeratoriEnumerator;

                 // -- is private
                 function getEnumeratorTiEnumerator<cg_cell<T> >;
                 function iEnumerable<cg_cell<T> >.GetEnumerator = GetEnumeratorT;
             end// c_find_list

    The GetEnumerator FUNCTION is obvious. But since .Net has a iEnumerable INTERFACE (in System.Collections) but also a "generic" iEnumerable INTERFACE (in System.Collections.Generic), when we implement the iEnumerable<T> INTERFACE in a CLASS we must implement both the generic and the non generic versions. This is done the two last FUNCTIONs of the CLASS.

  • our enumerator is here:

    type cig_cell_enumerator<T>=
             class(tObjectiEnumeratorcg_cell<T> >, iDisposable)
                 private
                   m_c_current_cellcg_cell<T>;
                   m_c_previous_cellcg_cell<T>;
                 strict private
                   function IEnumerator.get_Current = get_CurrentObject;
                   function get_CurrentObjectTObject;
                 public
                   function get_Currentcg_cell<T>;
                   constructor create_cell_enumerator(p_c_first_cellcg_cell<T> );

                   function MoveNextBoolean;
                   procedure Reset;
                   procedure Disposevirtual;

                   property Currentcg_cell<Tread get_Current;
               end// cig_cell_enumerator




The complete code of our iEnumerable list is:

unit u_c_genc_407_linked_ienumerable_list;
  interface
    uses System.CollectionsSystem.Collections.Generic;

type cg_cell<T> =
         class(System.Object)
           m_first_nameT;
           m_c_next_cellcg_cell<T>;

           constructor create_cell(p_first_nameTp_c_next_cellcg_cell<T>);
         end// c_find_list

     cig_cell_enumerator<T>=
         class(tObjectiEnumeratorcg_cell<T> >, iDisposable)
             private
               m_c_current_cellcg_cell<T>;
               m_c_previous_cellcg_cell<T>;
             strict private
               function IEnumerator.get_Current = get_CurrentObject;
               function get_CurrentObjectTObject;
             public
               function get_Currentcg_cell<T>;
               constructor create_cell_enumerator(p_c_first_cellcg_cell<T> );

               function MoveNextBoolean;
               procedure Reset;
               procedure Disposevirtual;

               property Currentcg_cell<Tread get_Current;
           end// cig_cell_enumerator

     cg_list<Tcg_cell<T> >=
         class(System.ObjectiEnumerable<cg_cell<T> > )
           private
             m_c_first_cellcg_cell<T>;
           public
             constructor create_list;

             function GetEnumeratoriEnumerator;
             // -- is private
             function getEnumeratorTiEnumerator<cg_cell<T> >;
             function iEnumerable<cg_cell<T> >.GetEnumerator = GetEnumeratorT;
         end// c_find_list

  implementation

    // -- cg_cell<T>

    constructor cg_cell<T>.create_cell(p_first_nameTp_c_next_cellcg_cell<T>);
      begin
        Inherited Create;

        m_first_name:= p_first_name;
        m_c_next_cell:= p_c_next_cell;
      end// create_cell

    // -- cig_cell_enumerator<T>

    constructor cig_cell_enumerator<T>.create_cell_enumerator(p_c_first_cellcg_cell<T> );
      begin
        Inherited Create;

        m_c_current_cell:= p_c_first_cell;
      end// create_cell_enumerator

    function cig_cell_enumerator<T>.Get_Currentcg_cell<T>;
      begin
        Result:= m_c_previous_cell;
      end// GetCurrent

    function cig_cell_enumerator<T>.get_CurrentObjectTObject;
      begin
        raise NotSupportedException.Create;
      end// get_CurrentObject

    function cig_cell_enumerator<T>.MoveNextBoolean;
      begin
        m_c_previous_cell:= m_c_current_cell;

        Result:= m_c_previous_cell<> Nil;
        if Result
          then m_c_current_cell:= m_c_current_cell.m_c_next_cell
      end// MoveNext

    procedure cig_cell_enumerator<T>.Reset;
      begin
        raise NotSupportedException.Create;
      end// reset

    procedure cig_cell_enumerator<T>.Dispose;
      begin
        raise NotSupportedException.Create;
      end// Dispose

    // --  cg_list<T>

    constructor cg_list<T>.create_list;
      begin
        Inherited Create;
      end// create_list

    function cg_list<T>.GetEnumeratoriEnumerator;
      begin
        raise NotSupportedException.Create;
      end// GetEnumerator

    function cg_list<T>.getEnumeratorTiEnumerator<cg_cell<T> >;
      var l_cig_cell_enumeratorcig_cell_enumerator<T>;
      begin
        l_cig_cell_enumerator:= cig_cell_enumerator<T>.create_cell_enumerator(m_c_first_cell);
      end// getEnumeratorT

  end.




6 - Some comments about Generics

6.1 - Generics Perspective

Generics are not a new concept. Languages like CLU already had generics. More close to Pascal, the Eiffel language had generics, and the Oberon language (Niklaus WIRTH's successor to Pascal and Modula) included them. The C++ language has the STL (Standard Template Library). And generics were introduced in the Java world (which was heavily inspired by Oberon), and from there into C#. Even UML did add a notation for type parameter !

All those versions of course have not the same functionality and implementation, but the underlying purpose is the same.



6.2 - What's missing

This presentation is not exhaustive. We did not present
  • the DEFAULT value of a generic type
  • nested CLASSes using generics
  • the full syntax diagram


6.3 - Libraries

Among the libraries that can be found using Google:
  • the C++ Stl (Standard Template Library). The Oberon developers stressed over and over again that the C++ style is only a pre-processor trick, and not a bona-fide compiled generic implementation.
  • in the .Net world:
    • the basic FCL (Framework Class Library) includes Queue<T>, ArrayList<T>, Dictionary<K, B>, LinkedList<V>
    • there are two independent libraries : C5 and nGenerics
  • Jedi certainly may implement generics, possibly deriving it from DCL (Delphi Container Library) from J.P. BEMPEL


6.4 - Our point of view

We are convinced that generics will be part of our daily programming tools, like Object Oriented programming. Therefore it is very important to get acquainted with them, with their strengths and weaknesses.

This is why we tried to present a diversified set of samples. To simply show some kind of generic stack or array is easy, and one could imagine the different syntactic possibilities and how the type parameter and type argument could be handled. But when we introduce several generic CLASSes, and mix genericity with inheritance, things get more involved.

But as with CLASS libraries, we only will have to dwelve into the generics intricacies if we undertake the building of some generic libraries. If this is not the case, we will simply use them. As a user it will be usefull to understand the different possibilities, some of which were presented here.



6.5 - What's next ?

Generics were introduced in Rad Studio 2007, since the underlying .Net 2.0 framework supports them.

The syntax has been slightly changed compared to the .Net version, which looks like (for a calculator operating on a list of values) :

class listsCalculator<T,C>
      where T: new()
      where C: ICalculator<T>, new();
    {
      ...
    }

whereas the current Delphi syntax would be like:

type c_list_calculator
        <
           T_dataCONSTRUCTOR
           T_calculatoriCalculatorT_data >, CONSTRUCTOR
        >
        = CLASS
            // -- ..ooo...
          END// c_list_calculator

It is possible that the Delphi syntax will revert to the more traditional .Net flavor when they will be implemented for the Win32 personality, sometime in 2008.



6.6 - No Generics Presentation at Ekon-Spring

The 24 January 2008 I received a german promotional e-mail titled "Liebe Entwickler ... Willkommen zur EKON Spring 2008 !". Der "Liber Entwickler" somehow felt maybe he could help. So, end of January, I offered to present the Genercis at this Ekon-Spring conference which was scheduled end of February. I surely was later than the "Call For Paper" deadline, but I naively assumed that when a product has 4 major innovations (dbx4, Blackfish, Asp.Net 20 and the Generics), it would not be too difficult to add an hour to the schedule, which as far as I know, did not include any Generics presentation. Well, I was badly wrong. So maybe next time, I'll try to be within deadlines, and they might consider my offer.

Meanwhile, since I still believe that Generics are a very important topic for Delphi developers, I took some of the examples I had prepared, and turned the slides into this article.




7 - Download the Generics Sample Demo Sources

Here are the source code files: The .ZIP file(s) contain:
  • the main program (.DPROJ, .DPR, .RES)
  • all units (.PAS) for units
All of the examples are .DPR console applications.

Those .ZIP

  • are self-contained: you will not need any other product (unless expressly mentioned).
  • will not modify your PC in any way beyond the path where you placed the .ZIP (no registry changes, no path outside from the container path creation etc).
To use the .ZIP:
  • create or select any folder of your choice.
  • unzip the downloaded file
  • add a ..\..\_EXE and ..\..\_EXE\DCU path
  • using Delphi, compile and execute
To remove the .ZIP simply delete the folder.

The Pascal code uses the Alsacian notation, which prefixes identifier by program area: K_onstant, T_ype, G_lobal, L_ocal, P_arameter, F_unction, C_lass etc. This notation is presented in the Alsacian Notation paper.



As usual:

  • please tell us at fcolibri@felix-colibri.com if you found some errors, mistakes, bugs, broken links or had some problem downloading the file. Resulting corrections will be helpful for other readers
  • we welcome any comment, criticism, enhancement, other sources or reference suggestion. Just send an e-mail to fcolibri@felix-colibri.com.
  • or more simply, enter your (anonymous or with your e-mail if you want an answer) comments below and clic the "send" button
    Name :
    E-mail :
    Comments * :
     

  • and if you liked this article, talk about this site to your fellow developpers, add a link to your links page ou mention our articles in your blog or newsgroup posts when relevant. That's the way we operate: the more traffic and Google references we get, the more articles we will write.



8 - References

Here are a couple of Internet references about generics:
  • Wikipedia overview of generics

  • .Net generics libraries:
  • for Delphi Rad Studio 2007 :
    • Parameterized types : Alan BAUER - Codegear - with a nice nested CLASS example - .SWF Video, 17 Mb
    • Generics : Ray KONOPKA - CodeRage II - .SWF Video, 17 MB, samples on Ray's site
    • the documentation contains a chapter about generics (Rad Studio | Rad Studio (Common) | Reference | Delphi Reference | Delphi Language Guide | Generics), derived from Yooichi Tagawa draft document, which mentions "Danny's draft plan". If this is Danny THORPE, this means that the Generics were cooking since a long time, before being implemented in Rad Studio 2007.


For (visitor, factory) design patterns you may look at:

Generics are also presented in our Rad Studio 2007 trainings, and specially in object oriented trainings and as a tool for buiding business rule abstraction layers (Data Abstraction Layer) in database applications.




9 - The author

Felix John COLIBRI works at the Pascal Institute. Starting with Pascal in 1979, he then became involved with Object Oriented Programming, Delphi, Sql, Tcp/Ip, Html, UML. Currently, he is mainly active in the area of custom software development (new projects, maintenance, audits, BDE migration, Delphi Xe_n migrations, refactoring), Delphi Consulting and Delph training. His web site features tutorials, technical papers about programming with full downloadable source code, and the description and calendar of forthcoming Delphi, FireBird, Tcp/IP, Web Services, OOP  /  UML, Design Patterns, Unit Testing training sessions.
Created: oct-07. Last updated: jul-15 - 98 articles, 131 .ZIP sources, 1012 figures
Copyright © Felix J. Colibri   http://www.felix-colibri.com 2004 - 2015. All rigths reserved
Back:    Home  Papers  Training  Delphi developments  Links  Download
the Pascal Institute

Felix J COLIBRI

+ Home
  + articles_with_sources
    + database
    + web_internet_sockets
    + oop_components
      – virtual_constructor
      – generics_tutorial
      – generics_constraints
      – livebindings_spelunking
    + uml_design_patterns
    + debug_and_test
    + graphic
    + controls
    + colibri_utilities
    + colibri_helpers
    + delphi
    + firemonkey
    + compilers
  + delphi_training
  + delphi_developments
  + sweet_home
  – download_zip_sources
  + links
Contacts
Site Map
– search :

RSS feed  
Blog